Bienvenidos a la Actividad 1, donde pondremos en práctica todo lo aprendido durante el bloque 2. Esta actividad la realizaremos en clase, se terminará en casa (debería completarse en clase) y se entregará el día 8 de octubre.
Vamos a poner en práctica cuatro aspectos del procesamiento de imágenes:
La finalidad es sencilla. Se os dará una imagen, a color, que tiene varias tonalidades y que está pintada con círculos.
La actividad consiste en contar el número de círculos de la imagen.
Se evaluará de la siguiente manera:
No se aceptará el formato .ipynb Habilitaré una actividad en Canvas para que podáis subir ambos archivos.
En primer lugar, cargamos todos los paquetes/frameworks que nos van a hacer falta. Se recomienda visitar la web: https://scikit-image.org/ para ver todas las funcionalidades que permite Scikit Image.
# Paquetes necesarios para la realización de esta práctica (no son necesarios conocerlos ni entenderlos por ahora)
from skimage.io import imread
from skimage import transform as tf
import matplotlib.pyplot as plt
# Cargamos la función para convertir de RGB a Escala de grises
from skimage.color import rgb2gray
# Paquete y funciones para realizar una umbralización con Scikit-image
from skimage.filters import threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola
# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle
# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
# Defino una función para mostrar una imagen por pantalla con el criterio que considero más acertado
def imshow(img, title):
fig, ax = plt.subplots(figsize=(7, 7))
# El comando que realmente muestra la imagen
ax.imshow(img,cmap=plt.cm.gray)
# Para evitar que aparezcan los números en los ejes
ax.set_xticks([]), ax.set_yticks([])
ax.set_title(title)
plt.show()
Lo primero de todo, vamos a leer la imagen. Recuerda que hay que subir la imagen cada vez que inicies sesión en el notebook y que la ruta se mira haciendo botón derecho sobre el archivo.
Con lo cual, aquí vamos a hacer dos cosas:
Hacemos esto para luego posteriormente umbralizar la imagen en escala de grises.
# Cargamos la imagen
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
imagen = 'Pintura_Puntos.jpg'
imagen = mpimg.imread(imagen)
plt.imshow(imagen)
plt.show()
#Convertimos la imagen a escala de grises
img_gris = rgb2gray(imagen)
imshow(img_gris, "Imagen en Escala de grises")
Vamos a probar ahora diferentes métodos para umbralizar la imagen. Se pide en esta actividad:
Método OTSU: Calcula automáticamente un umbral óptimo para separar los objetos de interés del fondo en una imagen en escala de grises.
#Empleamos el método OTSU en la imagen de escala de grises
otsu = threshold_otsu(img_gris)
imagen_otsu = img_gris < otsu
plt.imshow(imagen_otsu, cmap=plt.cm.gray)
plt.title('Método de Otsu')
plt.show()
Método de Niblack: Divide la imagen en pequeñas ventanas o bloques y calcula un umbral local para cada bloque.
Podemos observar que cambia mucho el resultado dependiendo el tamaño de la ventana que pongamos; es decir para números pequeños se ve con mejor calidad la imagen que con los números grandes.
# Defino el tamaño de la ventana para numeroso bajos (19)
window_size = 19
#Defino el tamaño de la k
k = -0.2
imagen_niblack = threshold_niblack(img_gris, window_size=window_size, k=k)
plt.imshow(imagen_niblack, cmap=plt.cm.gray)
plt.title('Método Niblack')
plt.show()
# El tamaño de la ventana para números grandes (79)
window_size = 79
#Defino el tamaño de la k
k = -0.2
imagen_niblack = threshold_niblack(img_gris, window_size=window_size, k=k)
plt.imshow(imagen_niblack, cmap=plt.cm.gray)
plt.title('Método Niblack')
plt.show()
Método de Local: Los métodos locales suelen utilizarse para resaltar características específicas, reducir el ruido o adaptar el procesamiento a variaciones locales en la imagen.
Es mejor usar bloque grande para que se defina mejor la imagen quitando el ruido.
#Definimos el tamaño del bloque con número grande
block_size = 73
local = threshold_local(img_gris, block_size)
img_local = img_gris > local
plt.imshow(img_local, cmap=plt.cm.gray)
plt.title('Método de Local')
plt.show()
#Definimos el tamaño del bloque con número pequeño
block_size = 21
local = threshold_local(img_gris, block_size)
img_local = img_gris > local
plt.imshow(img_local, cmap=plt.cm.gray)
plt.title('Método de Local')
plt.show()
Método de Sauvola: Al igual que Niblack, divide la imagen en bloques y calcula umbrales locales. Sin embargo, Sauvola utiliza un enfoque adaptativo que tiene en cuenta la media y la desviación estándar locales de la intensidad de los píxeles, lo que lo hace robusto frente a cambios en la iluminación y el contraste local.
imagen_sauvola = threshold_sauvola(img_gris)
plt.imshow(imagen_sauvola, cmap=plt.cm.gray)
plt.title('Método Sauvola')
plt.show()
El método que mas se parece al resultado que hay que alcanzar, es el Método Local. Es por ello que voy a coger el Local para seguir realizando el trabajo ya que se perciben mejor los puntos para proceder a contarlos.
Como se puede apreciar en la imagen hay varios elementos imperfectos:
Mediante el uso de morfología matemática (concretamente los cuatro operadores visto en clase) y los posibles elementos estructurales existentes, se pide:
def plot_comparison(original, filtered, filter_name):
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(20, 10), sharex=True,
sharey=True)
ax1.imshow(original, cmap=plt.cm.gray)
ax1.set_title('Imagen Original')
ax1.axis('off')
ax1.set_adjustable('box')
ax2.imshow(filtered, cmap=plt.cm.gray)
ax2.set_title(filter_name)
ax2.axis('off')
ax2.set_adjustable('box')
plt.show()
# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle
# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
Erosión: El resultado es que la erosión tiende a eliminar detalles pequeños, eliminar ruido y reducir el tamaño de los objetos en una imagen.
elemento_estructural_disco = disk(5)
img_eroded = erosion(img_local,elemento_estructural_disco)
plot_comparison(img_local,img_eroded,'Erosión')
Dilatación: El resultado es que la dilatación aumenta el tamaño de los objetos en una imagen y llena agujeros pequeños o separaciones en las regiones.
elemento_estructural_disco = disk(5)
img_dilation = dilation(img_local,elemento_estructural_disco)
plot_comparison(img_local,img_dilation,'Dilation')
Apertura: La apertura es una operación morfológica en procesamiento de imágenes que combina dos operaciones morfológicas básicas: la erosión seguida de la dilatación.
opening_img = opening(img_local,disk(4))
plot_comparison(img_local,opening_img,'Opening')
Clausura: La clausura (también conocida como cierre) es una operación morfológica en procesamiento de imágenes que se utiliza para completar y cerrar pequeños agujeros o huecos en objetos en una imagen.
closing_img = closing(1-img_local,disk(5))
plot_comparison(img_local,closing_img,'Closing')
Erosión
elemento_estructural_diamante = diamond(4)
img_eroded2 = erosion(img_local,elemento_estructural_diamante)
plot_comparison(img_local,img_eroded2,'Erosión')
Dilatación
elemento_estructural_diamante = diamond(2)
img_dilation2 = dilation(img_local,elemento_estructural_diamante)
plot_comparison(img_local,img_dilation2,'Dilation')
Apertura
elemento_estructural_diamante = diamond(4)
img_opening2 = opening(img_local,elemento_estructural_diamante)
plot_comparison(img_local,img_opening2,'Opening')
Clausura
elemento_estructural_diamante = diamond(4)
img_closing2 = closing(img_local,elemento_estructural_diamante)
plot_comparison(img_local,img_closing2,'Closing')
Como he comentado anteriormente el método local es el ideal para contar los círculos. Previamente después de haber aplicado los diferentes filtros podemos observar: que con el elemento Disk la imagen empeorá y no desparacen la gran mayoría de puntos, por tanto, no es útil a la hora de contar; en cambio con el elemento Diamond podemos observar que se cuentan más facilmente los puntos, sobretodo con el filtro de apertura.
Haciendo uso de las funcionalidades cargadas al principio, se pide hacer una función que:
Por último, ¿qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?
Conversión de la imagen Local en binaria
import numpy as np
# Hay que convertir la imagen en escala de grises a una matriz NumPy
img_local_np = np.array(img_local)
import numpy as np
import matplotlib.pyplot as plt
import imageio
# Convertir la matriz en binaria
img_local_binaria = np.where(img_local_np, 255, 0).astype(np.uint8)
plt.imshow(img_local_binaria, cmap='gray')
plt.show()
Conversión de la Imagen de Apertura (diamante) en binaria
img_opening2_np = np.array(img_opening2)
img_opening2_binaria = np.where(img_opening2_np, 255, 0).astype(np.uint8)
plt.imshow(img_opening2_binaria, cmap='gray')
plt.show()
Conversión Imagen Apertura (Disk) en binaria
opening_img_np = np.array(opening_img)
opening_img_binaria = np.where(opening_img_np, 255, 0).astype(np.uint8)
plt.imshow(opening_img_binaria, cmap='gray')
plt.show()
Comprobación de matrices (local y apertura)
img_local_np, img_local_binaria
(array([[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., False, False, False],
...,
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True]]),
array([[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 0, 0, 0],
...,
[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255]], dtype=uint8))
img_opening2_np, img_opening2_binaria
(array([[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
...,
[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False]]),
array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=uint8))
Contamos los círculos (local y apertura)
# Función para contar el número de círculos
import cv2
def count_circles(imagen_binaria):
# Hay que comprobar si la imagen es binaria
if len(imagen_binaria.shape) > 2 or set(np.unique(imagen_binaria)) != {0, 255}:
print("La imagen no es binaria.")
return None
# Encontrar los contornos en la imagen
contours, _ = cv2.findContours(imagen_binaria, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Contar el número de círculos
num_circulos = 0
for contour in contours:
# Aproximar el contorno a una forma más simple (círculo) utilizando un polígono
approx = cv2.approxPolyDP(contour, 0.01 * cv2.arcLength(contour, True), True)
# Si el polígono tiene más de ciertos puntos, considerarlo como un círculo
if len(approx) >= 8:
num_circulos += 1
return num_circulos
Imagen Local
plt.imshow(img_local_binaria, cmap='gray')
plt.show()
count_circles(img_local_binaria)
6400
Imagen Apertura (diamantes)
plt.imshow(img_opening2_binaria, cmap='gray')
plt.show()
count_circles(img_opening2_binaria)
5354
Conclusión: Como podemos observar se obtiene mejor resultado usando la imagen Local sin ningún filtro, que usando apertura con el filtro diamante debido a que el número de conteo es menor en este último. Por tanto, concluimos con que la imagen es más util sin filtro que con el filtro diamond. Ahora probamos el filtro Disk para compararlo con el Diamond.
Imagen Apertura (Disk)
plt.imshow(opening_img_binaria, cmap='gray')
plt.show()
count_circles(opening_img_binaria)
6632
Como podemos observar en comparación con el elemento Diamante, es mejor el elemento Disk (tiene sentido ya que en este tipo de imagen lo que queremos es contar elementos circulares), por tanto es mejor Disk. As u vez, hemos podido observar que también es mejor la imagen con el elemento Disk y el filtro Apertura que incluso la propia imagen Local que al principio nos parecía la idonea.
Esta sección no es obligatoria pero la pongo para aquellos que quieran saber "¿y ahora qué se haría?".
Lo que hemos hecho hasta ahora es:
Es decir, tenemos varios parámetros y tenemos una función que nos dice cuál es el número de puntos dada una imagen. Variando dichos parámetros, variará también el número de puntos, pero no parece haber una relación directa.
También no hay que olvidar que desconocemos el número de puntos (nunca se ha dicho, aunque siempre puedes contarlos), por lo que no podemos seguir un proceso de aprendizaje supervisado (tipo descenso del gradiente sobre los parámetros anteriores para encontrar el mejor resultado).
Pero lo que sí podemos hacer es iterar el valor de los parámetros para alcanzar un máximo de puntos (asumiendo que dicho máximo corresponderá con el mejor resultado). Esto suele hacerse cuando no sabemos exáctamente el resultado que esperamos.
En definitiva, ahora se buscaría realizar un proceso iterativo para encontrar el valor máximo del número de puntos. Para ello haría falta:
product del paquete itertools).Podría decirse que esa combinación de parámetros es la mejor.
import itertools
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Cargar la imagen y convertirla a escala de grises
imagen = 'Pintura_Puntos.jpg'
img_gris = cv2.imread(imagen, cv2.IMREAD_GRAYSCALE)
# Función para contar el número de círculos con parámetros ajustables
def count_circles_with_parameters(imagen_binaria, umbral, tamano_kernel):
# Aplicar umbralización a la imagen
_, imagen_umbral = cv2.threshold(imagen_binaria, umbral, 255, cv2.THRESH_BINARY)
# Aplicar operaciones de morfología matemática con el tamaño del kernel especificado
kernel = np.ones((tamano_kernel, tamano_kernel), np.uint8)
imagen_morfologia = cv2.morphologyEx(imagen_umbral, cv2.MORPH_OPEN, kernel)
# Contar el número de círculos en la imagen procesada
num_circulos = count_circles(imagen_morfologia)
return num_circulos
# Define los rangos de valores para los parámetros
rangos_umbral = [100, 120, 140] # Ejemplo de valores de umbral
rangos_tamano_kernel = [3, 5, 7] # Ejemplo de tamaños de kernel
# Inicializa variables para mantener un seguimiento de la combinación óptima
max_puntos = 0
mejor_combinacion = None
# Itera a través de todas las combinaciones de parámetros
for umbral, tamano_kernel in itertools.product(rangos_umbral, rangos_tamano_kernel):
# Procesa la imagen actual con la combinación de parámetros actual
puntos_detectados = count_circles_with_parameters(img_gris, umbral, tamano_kernel)
# Verifica si esta combinación de parámetros produce más puntos que la máxima registrada
if puntos_detectados > max_puntos:
max_puntos = puntos_detectados
mejor_combinacion = (umbral, tamano_kernel)
# Al final del bucle, tendrás la combinación de parámetros óptima
print("Combinación óptima de parámetros:")
print("Umbral:", mejor_combinacion[0])
print("Tamaño del kernel:", mejor_combinacion[1])
print("Número máximo de puntos detectados:", max_puntos)
Combinación óptima de parámetros: Umbral: 140 Tamaño del kernel: 7 Número máximo de puntos detectados: 1991
Este es el resultado del código, el número ideal de puntos es 1991 todo basado en la imagen binaria de la escala de grises. El kernel debe sr 7 y el umbral 140.